Skip to content

fix(tfchain-client-js): fix stale runtime assumptions in read wrappers (twin ip, entity storage, null crashes)#1101

Merged
sameh-farouk merged 3 commits into
developmentfrom
fix/js-client-hex2a-stale-twin-ip
Jun 2, 2026
Merged

fix(tfchain-client-js): fix stale runtime assumptions in read wrappers (twin ip, entity storage, null crashes)#1101
sameh-farouk merged 3 commits into
developmentfrom
fix/js-client-hex2a-stale-twin-ip

Conversation

@sameh-farouk
Copy link
Copy Markdown
Member

@sameh-farouk sameh-farouk commented Jun 2, 2026

Closes #1090, #1102. Expanded from the original twin-ip fix after live devnet testing surfaced a cluster of bugs of the same class — stale runtime assumptions in the barely-maintained read wrappers. Every fix below is verified live against devnet (spec 157).

Bugs fixed

1. twin ip crash (#1090)getTwin/listTwins decoded a non-existent ip field; hex2a(undefined) threw. Removed the stale decode and hardened hex2a to return '' on undefined/null (the root of the crash class — hex2a is called on ~16 fields, several now Option/null).

2. Entity lookups queried dead storage (critical — broke activation-service)

  • getEntityIDByNameentitiesByNameID (gone) → entityIdByName
  • getEntityIDByPubkeyentitiesByPubkeyID (gone) → entityIdByAccountID
  • Both threw is not a function. activation-service's createEntity calls both, so it was broken against the current runtime. Also normalize not-found → 0 (new storage decodes to null).

3. Null crashes on non-existent idsgetEntity/getNode/getTwin did res.id on a null decode. Guarded to throw a clean No such <x>.

4. getNode returned un-decoded data (wrong key + wrong structure)

  • Public config: read res.public_config (snake, doesn't exist) with a flat {ipv4,ipv6,gw4,gw6} shape → nothing decoded. Fixed to publicConfig with the real nested shape { ip4:{ip,gw}, ip6:{ip,gw}, domain }.
  • city/country are nested under location (not top-level) → never decoded. Now decoded inside location alongside lat/long.

5. getPricingPolicyById returned the policy name as raw hex → now hex2a-decoded.

Live verification (devnet, spec 157)

getEntityIDByName("x")    -> 0      (was: not a function)
getEntityIDByPubkey(acc)  -> 0      (was: not a function)
getEntityByID(1)          -> throws "No such entity"  (was: null crash)
getNodeByID(1)            -> throws "No such node"    (was: null crash)
getTwinByID(999999)       -> throws "No such twin"    (was: null crash)
getTwinByID(1)            -> {id:1, ..., relay:null, pk:null}            ✓
listTwins()               -> 21166 twins, no crash                       ✓
getNodeByID(50).location  -> {city:"Bartlesville", country:"United States", lat/long}  ✓ (was hex)
getNodeByID(50).publicConfig -> {ip4:{ip:"162.205.240.200/25",gw:...}, ip6, domain:"virt3.tfcloud.us"}  ✓ (was hex)
getPricingPolicyById(1).name -> "threefold_default_pricing_policy"        ✓ (was hex)
getBalanceOf, getFarmByID, getContractByID                                ✓

Note correcting the activation-service compatibility assessment

Earlier static analysis concluded activation-service was API-compatible with the v2 client because the method names exist. Live testing disproved that for createEntity: the methods existed but queried dead storage. This PR is what actually makes the client work against the current runtime — directly relevant to the activation-service v2 upgrade (#1099).

Tests

  • test/util.test.js for hex2a (built-in node:test, no new deps); test script wired up.
  • The read wrappers need a live api, so they're covered by the devnet runs above rather than unit tests.

🤖 Generated with Claude Code

…ield

The Twin struct no longer has an `ip` field (migrated to `relay`/`pk` long
ago), but getTwin/listTwins still did `res.ip = hex2a(res.ip)`. hex2a(undefined)
dereferences `undefined.length`, so every getTwinByID/listTwins call threw
`Cannot read properties of undefined (reading 'length')` on the current runtime.

- Remove the stale `ip` decode from getTwin and listTwins; return the
  metadata-decoded object as-is.
- Harden hex2a to return '' for undefined/null. This is the root of the same
  crash class: hex2a is called on ~16 fields across twin/entity/farms/node/
  tfkvstore, several of which are Option<...> (null) on the current runtime
  (e.g. node serialNumber, publicConfig ipv4/ipv6/gw4/gw6). Null-safe hex2a
  immunizes all of them.
- Add unit tests for hex2a (built-in node:test, no new deps) and wire up the
  `test` script.

Closes #1090

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@sameh-farouk sameh-farouk requested a review from LeeSmet as a code owner June 2, 2026 10:56
…uard reads

Live devnet testing (spec 157) surfaced more stale-runtime-assumption bugs in
the read wrappers, beyond the twin `ip` field:

- getEntityIDByName queried `entitiesByNameID` and getEntityIDByPubkey queried
  `entitiesByPubkeyID` — neither exists on the current runtime (renamed to
  `entityIdByName` / `entityIdByAccountID`). Both threw "is not a function".
  These are called by activation-service's createEntity, so createEntity was
  broken against the current runtime. Also normalize not-found to 0 (the new
  storage decodes to null) to preserve the "0 means absent" contract callers rely on.
- getEntity / getNode / getTwin did `res.id` on a null result when the id does
  not exist, throwing "Cannot read properties of null". Guard with `!res` so they
  throw a clean "No such <x>" instead.

All verified live on devnet.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@sameh-farouk sameh-farouk changed the title fix(tfchain-client-js): stop getTwin/listTwins crashing on stale ip field fix(tfchain-client-js): fix stale runtime assumptions in read wrappers (twin ip, entity storage, null crashes) Jun 2, 2026
…g policy name

More stale-structure bugs found while live-verifying getNode/getPricingPolicyById
on devnet (all returned un-decoded hex):

- getNode public config: read the field as `publicConfig` (camelCase, was the
  non-existent `public_config`) and decode its actual nested shape
  { ip4: { ip, gw }, ip6: { ip, gw }, domain } (was a flat { ipv4, ipv6, gw4, gw6 }
  that matched nothing).
- getNode location: city/country are nested under `location` on the current
  runtime (not top-level), so they were never decoded. Decode city/country/
  latitude/longitude inside `location`; drop the dead top-level country/city reads.
- getPricingPolicyById: decode the byte-encoded `name` (was returned as raw hex).

Verified live on devnet: node 50 now returns city "Bartlesville", country
"United States", public IPs/domain readable; policy 1 name "threefold_default_pricing_policy".

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant